iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Software Development

我的SpringBoot絕學:7+2個專案,從新手變專家系列 第 28

Day28 前端專案:Vue.js(2)持久化保存與商品頁面

  • 分享至 

  • xImage
  •  

持久化保存

我們現在遇到一個大問題,登入後切換頁面,就會讓token消失,使用router.push可以暫時解決,但按F5重新整理也會讓token消失。

最終,我們必須將token持久化保存,這樣就能保存一輩子的token。


安裝pinia持久化套件

bun install pinia-plugin-persistedstate

修改main.js,啟用pinia持久化

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

pinia.use(piniaPluginPersistedstate)

修改auth.js,將token持久化

export const useAuthStore = defineStore('auth', {
  state: () => ({
    //...
  }),
  getters: {
    //...
  },
  actions: {
    //...
  },
  persist: true,//啟用pinia持久化
});

重新整理、切換頁面token都不會消失了。

注意:token過期後,會因為持久化保存殘留token,需要手動logout來清除token。

新增商品頁面

建立AddProduct.vue,大部分的內容來自LoginForm.vue,只是需要多幾個div,修改axios的URL,傳送時加上token。

因為我們在SpringBoot的設定,為了一次接收多筆產品資料,使用了Product[],因此data需要多出[],就算只傳一筆也要加[]。

<template>
	<div className="flex justify-center min-h-screen items-center bg-gray-100">
		<form
			method="post"
			role="form"
			className="bg-white p-6 rounded-lg shadow-md w-full max-w-md"
			@submit.prevent="handleSubmit"
		>
			<div className="mb-4">
				<label className="text-gray-700 font-bold mb-2"> Name </label>
				<input
					placeholder="Enter product name"
					type="text"
					className="shadow border rounded w-full py-2 px-3 text-gray-700"
					name="name"
					v-model="name"
				/>
			</div>

			<div className="mb-4">
				<label className="text-gray-700 font-bold mb-2"> Description </label>
				<input
					placeholder="Enter product description"
					type="text"
					className="shadow border rounded w-full py-2 px-3 text-gray-700"
					name="description"
					v-model="description"
				/>
			</div>

			<div className="mb-4">
				<label className="text-gray-700 font-bold mb-2"> Price </label>
				<input
					placeholder="Enter product price"
					type="text"
					className="shadow border rounded w-full py-2 px-3 text-gray-700"
					name="price"
					v-model="price"
				/>
			</div>

			<div className="mb-4">
				<label className="text-gray-700 font-bold mb-2"> Image URL </label>
				<input
					placeholder="Enter product image URL"
					type="text"
					className="shadow border rounded w-full py-2 px-3 text-gray-700"
					name="image"
					v-model="image"
				/>
			</div>

			<div className="mb-4">
				<label className="text-gray-700 font-bold mb-2"> Category </label>
				<input
					placeholder="Enter product category"
					type="text"
					className="shadow border rounded w-full py-2 px-3 text-gray-700"
					name="category"
					v-model="category"
				/>
			</div>

			<button
				type="submit"
				className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
			>
				Add
			</button>
		</form>
	</div>
</template>

<script>
import { ref } from "vue";
import { useRouter } from "vue-router";
import axios from "axios";
import { useAuthStore } from "@/stores/auth";

export default {
	setup() {
		const router = useRouter();
		const authStore = useAuthStore();
		const name = ref("");
		const description = ref("");
		const price = ref("");
		const image = ref("");
		const category = ref("");

		const handleSubmit = async () => {
			try {
				const token = authStore.token;
				if(!name.value || !description.value || !price.value || !image.value || !category.value || isNaN(price.value)){
					throw new Error("商品資料格式錯誤");
				}
				const data = [{
					name: name.value,
					description: description.value,
					price: parseInt(price.value),
					image: image.value,
					category: category.value,
				}];
				const response = await axios.post(
					"http://localhost:8080/api/product/",
					data,
					{
						headers: {
							'Authorization': `Bearer ${token}`
						}
					}
				);
				console.log(response.data);
				router.push("/");
			} catch (error) {
				
				alert("Error");
				console.log(error);
			}
		};

		return {
			name,
			description,
			price,
			image,
			category,
			handleSubmit,
		};
	},
};
</script>

在router/index.js添加路由

  routes: [
    {
      //
    },
    {
      //
    },
    {
      path: '/add',
      name: 'add',
      component: AddProduct
    }
  ]

點擊Add Product, 嘗試新增商品資料。

也測試沒填內容或不是數字的價格,會彈出Error視窗。

首頁呈現商品內容

新增HomePage.vue,用來顯示商品列表。

<template>
	<div v-if="isAuthenticated" class="p-4 bg-gray-50 min-h-screen">
		<div
			v-for="product in products"
			:key="product.id"
			class="flex items-center border-b border-gray-200 py-4"
		>
			<img
				:src="product.image"
				:alt="product.name"
				class="w-16 h-16 object-cover rounded-lg shadow-sm mr-4"
			/>
			<div class="flex-1">
				<h2 class="text-lg font-bold text-gray-900">{{ product.name }}</h2>
				<p class="text-gray-700 text-sm">{{ product.description }}</p>
				<p class="text-green-600 font-semibold text-sm mt-1">
					Price: {{ product.price }}
				</p>
				<p class="text-gray-500 text-xs mt-1">
					Category: {{ product.category }}
				</p>
			</div>
		</div>
	</div>
	<div v-else class="p-4">
		<p class="text-red-600 font-medium">Please log in to see the products.</p>
	</div>
</template>

<script setup>
import { useAuthStore } from "@/stores/auth";
import { computed, onMounted, ref } from "vue";
import axios from "axios";

const authStore = useAuthStore();
const isAuthenticated = computed(() => authStore.isAuthenticated);
//將商品資料提供給template
const products = ref([]);

const getProducts = async () => {
	try {
		const response = await axios.get(
			"http://localhost:8080/api/product/?minPrice=&maxPrice=&category=&sort=&pageNumber=0&pageSize=20",
			{
				headers: {
					Authorization: `Bearer ${authStore.token}`,
				},
			}
		);
		//把取得的商品資料放入products
		products.value = response.data.content;
		console.log(response.data);
	} catch (error) {
		console.log(error);
	}
};

//等待template載入完成,再把商品資料放入template
onMounted(() => {
	if (isAuthenticated.value) {
		getProducts();
	}
});
</script>

在router/index.js添加路由

  routes: [
    {
      //...
    },
    //...
    {
      path: '/',
      name: 'home',
      component: HomePage
    }
  ]

登入後就能看到資料庫的商品。

換頁

接下來,我們完成換頁的功能。


安裝vue-awesome-paginate

bun i vue-awesome-paginate

我們參考官方提供的範例https://www.npmjs.com/package/vue-awesome-paginate#usage,修改HomePage.vue

<template>
	<div v-if="isAuthenticated" class="p-4 bg-gray-50 min-h-screen">

		<div
			v-for="product in products"
			:key="product.id"
			class="flex items-center border-b border-gray-200 py-4"
		>
			//...
		</div>
		<div class="flex justify-center">
			<vue-awesome-paginate
				:total-items="50"
				:items-per-page="5"
				:max-pages-shown="5"
				v-model="currentPage"
				@click="onClickHandler"
			/>
		</div>
	</div>
	<div v-else class="p-4">
		//...
	</div>
</template>

<script setup>
//...

const onClickHandler = (page) => {
	console.log(page);
};
const currentPage = ref(1);

onMounted(() => {
	//...
});
</script>

<style>
.pagination-container {
	display: flex;

	column-gap: 10px;
}

.paginate-buttons {
	height: 40px;

	width: 40px;

	border-radius: 20px;

	cursor: pointer;

	background-color: rgb(242, 242, 242);

	border: 1px solid rgb(217, 217, 217);

	color: black;
}

.paginate-buttons:hover {
	background-color: #d8d8d8;
}

.active-page {
	background-color: #3498db;

	border: 1px solid #3498db;

	color: white;
}

.active-page:hover {
	background-color: #2988c8;
}
</style>

修改main.js,增加

import VueAwesomePaginate from 'vue-awesome-paginate';
import "vue-awesome-paginate/dist/style.css";

//...

app.use(VueAwesomePaginate)

找到選擇頁面的地方,按下<、>和數字,觀察Console顯示的內容,是否和選擇的頁數相同。

我們按下2,Console也顯示2。

更改每頁顯示的數量

我們增加一個可以選擇顯示商品數量的地方

<template>
//...
<div class="flex justify-center">
			<vue-awesome-paginate
				:total-items="50"
				:items-per-page="5"
				:max-pages-shown="5"
				v-model="currentPage"
				@click="onClickHandler"
			/>
				<select class="ml-4 bg-gray-200" name="pageSize" id="pageSize" v-model="pageSize" @change="onChangeHandler">
					<option value="5">5</option>
					<option value="10">10</option>
					<option value="15">15</option>
					<option value="20">20</option>
				</select>
		</div>
</template>

<script setup>
//...
const pageSize = ref(5);

const onChangeHandler = () => {
	console.log(pageSize.value);
};
</script>

我們依序選擇10、15、20、5,在內容變化時,能在Console看到對應的輸出。

因為預設值是5,選5的話,內容沒有變化,必須選10、15、20才符合變化的條件。


接下來,讓頁面的內容隨著我們的點擊改變。

讓vue-awesome-paginate,接收來自後端的總商品數量,調整顯示最後一頁的頁碼。

<template>
//...
<div class="flex justify-center">
			<vue-awesome-paginate
				:total-items="totalElement"
				:items-per-page="parseInt(pageSize)"
				:max-pages-shown="5"
				v-model="currentPage"
				@click="onClickHandler"
			/>
				<select class="ml-4 bg-gray-200" name="pageSize" id="pageSize" v-model="pageSize" @change="onChangeHandler">
					<option value="5">5</option>
					<option value="10">10</option>
					<option value="15">15</option>
					<option value="20">20</option>
				</select>
		</div>
		//...
</template>

修改axios根據前端設定的頁碼和每頁數量,取得商品資料,頁數因為資料庫從0開始,網頁從1開始,所以要-1。

<script setup>
//...
const totalElement = ref(1);

const getProducts = async (page, size) => {
	try {
		const response = await axios.get(
			`http://localhost:8080/api/product/?minPrice=&maxPrice=&category=&sort=&pageNumber=${page - 1}&pageSize=${parseInt(size)}`,
			{
				headers: {
					Authorization: `Bearer ${authStore.token}`,
				},
			}
		);
		//把取得的商品資料放入products
		products.value = response.data.content;
		totalElement.value = response.data.totalElements;
		console.log(response.data);
	} catch (error) {
		console.log(error);
	}
};

當切換頁碼和改變每頁顯示數量時,內容也要更新

const onClickHandler = () => {
	getProducts(currentPage.value, pageSize.value);
};

const onChangeHandler = () => {
	currentPage.value = 1;
	getProducts(currentPage.value, pageSize.value);
};

因為我們修改了getProducts的傳入參數,onMounted的部分增加參數

onMounted(() => {
	if (isAuthenticated.value) {
		getProducts(currentPage.value, pageSize.value);
	}
});
</script>

在前端確認修改結果,可以看到我們選擇5,網頁上顯示的商品數量也是5個。

根據條件篩選商品

我們將在左邊放置篩選用的列表,右邊放商品內容。

  • 在v-if="isAuthenticated”的class添加flex
  • 增加div class="w-1/4 p-4",用來放置過濾區塊,占用1/4寬度
  • 在顯示商品的外面,多加一層div class="w-3/4 p-4",讓它能使用3/4寬度的版面
<template>
	<div v-if="isAuthenticated" class="p-4 bg-gray-50 min-h-screen flex">
		<div class="w-1/4 p-4">
			<div class="p-4 bg-gray-100 rounded-lg shadow-md">
				<div class="mb-4">
					<label class="block text-gray-700 font-bold mb-2">Price</label>
					<div class="flex items-center">
						<input
							placeholder="min"
							type="number"
							class="border p-2 mb-2 w-full mr-2 rounded"
							v-model="minPrice"
						/>
						<span class="mx-2">to</span>
						<input
							placeholder="max"
							type="number"
							class="border p-2 mb-2 w-full ml-2 rounded"
							v-model="maxPrice"
						/>
					</div>
				</div>
				<div class="mb-4">
					<label class="block text-gray-700 font-bold mb-2">Sort by:</label>
					<select class="border p-2 mb-4 w-full rounded" v-model="sortBy">
						<option value="">Default</option>
						<option value="price_low">Price : Low to High</option>
						<option value="price_high">Price : High to Low</option>
					</select>
				</div>
				<div class="mb-4">
				<label class="block text-gray-700 font-bold mb-2">Category:</label>
				<div v-for="category in categories" :key="category" class="mb-2">
					<input
						type="radio"
						:value="category"
						v-model="categoryType"
						class="mr-2"
					/>
					<label class="text-gray-700">{{ category ? category : "All" }}</label>
				</div>
				</div>
				<button
					class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded w-full"
					@click="filterHandler"
				>
					Filter
				</button>
			</div>
		</div>
		<div class="w-3/4 p-4">
			<div
				v-for="product in products"
				:key="product.id"
				class="flex items-center border-b border-gray-200 py-4"
			>
				//...
			</div>
			//...
		</div>
	</div>
	<div v-else class="p-4">
		<p class="text-red-600 font-medium">Please log in to see the products.</p>
	</div>
</template>

<script setup>
//...
const categories = [
	"",
	"Bags",
	"Shoe",
	"Headset",
	"Mice",
	"Keyboard",
	"Chair",
	"Dinnerware",
	"Light",
	"Jean",
];
const categoryType = ref("");
const minPrice = ref("");
const maxPrice = ref("");
const sortBy = ref("");

//...

const filterHandler = () => {
	console.log(minPrice.value, maxPrice.value, categoryType.value, sortBy.value);
};

//...
</script>

填寫最低價、最高價、排序方式、商品類別後,按下Filter按鈕,就能看到預計傳給後端的參數。

可以看到和我們選擇的內容一樣,有相同的數值。


確認參數沒問題後,完成過濾商品功能。

修改getProducts的輸入參數,使用到getProducts的地方也要增加傳入的參數。

const getProducts = async (min, max, category, sort, page, size) => {
	try {
		const response = await axios.get(
			`http://localhost:8080/api/product/?minPrice=${
				min
			}&maxPrice=${
				max
			}&category=${category}&sort=${sort}&pageNumber=${
				page - 1
			}&pageSize=${parseInt(size)}`,
			{
				headers: {
					Authorization: `Bearer ${authStore.token}`,
				},
			}
		);
		products.value = response.data.content;
		totalElement.value = response.data.totalElements;
		console.log(response.data);
	} catch (error) {
		console.log(error);
	}
};
const onClickHandler = () => {
	getProducts(minPrice.value, maxPrice.value, categoryType.value, sortBy.value, currentPage.value, pageSize.value);
};

const onChangeHandler = () => {
	currentPage.value = 1;
	getProducts(minPrice.value, maxPrice.value, categoryType.value, sortBy.value, currentPage.value, pageSize.value);
};

const filterHandler = () => {
	currentPage.value = 1;
	getProducts(minPrice.value, maxPrice.value, categoryType.value, sortBy.value, currentPage.value, pageSize.value);
};

onMounted(() => {
	if (isAuthenticated.value) {
		getProducts(minPrice.value, maxPrice.value, categoryType.value, sortBy.value, currentPage.value, pageSize.value);
	}
});

我們可以修改最低價、最高價、排序設定、商品類別,觀察商品的變化。

  • 設定200-999、Price:Low To High、Category:All

顯示價格從200到999的商品,價格由低到高排序。

  • 設定699-699、Price:High To Low 、Category:Keyboard

只會顯示699的Keyboard商品


上一篇
Day27 前端專案:Vue.js(1)完成註冊、登入與登出
下一篇
Day29 前端專案:Vue.js(3)購物車和導入SSL
系列文
我的SpringBoot絕學:7+2個專案,從新手變專家31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言